Gestures for Globes Research

Data and Libraries Load

library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ ggplot2   3.5.1     ✔ tibble    3.2.1
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
## ✔ purrr     1.0.4     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(dplyr)
library(car)
## Loading required package: carData
## 
## Attaching package: 'car'
## 
## The following object is masked from 'package:dplyr':
## 
##     recode
## 
## The following object is masked from 'package:purrr':
## 
##     some
library(ggplot2)
library(lubridate)
library(scales)
## 
## Attaching package: 'scales'
## 
## The following object is masked from 'package:purrr':
## 
##     discard
## 
## The following object is masked from 'package:readr':
## 
##     col_factor
library(ARTool)
library(knitr)

data <- read_csv("study_tasks.csv")
## Rows: 49244 Columns: 35
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr   (9): TaskID, ActionID, distance, direction, complexity, zoomDirection,...
## dbl  (22): UserID, main_translation_x, main_translation_y, main_translation_...
## lgl   (3): rotateGlobeWhileDragging, oneHandedRotationGesture, moveGlobeWhil...
## dttm  (1): Date
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
demographic <- read_csv("final_introductory.csv")
## Rows: 12 Columns: 8
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (7): Timestamp, Academic_level, Gender, Age_group, Exp_ARVR, Globe_usage...
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
positioning_NRG <- read_csv("final_positioning_NRG.csv")
## Rows: 12 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): Timestamp, Mentally_demanding, Physically_demanding
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
positioning_RG <- read_csv("final_positioning_RG.csv")
## Rows: 12 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): Timestamp, Mentally_demanding, Physically_demanding
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
positioning_preference <- read_csv("final_positioning_comparison.csv")
## Rows: 12 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): Timestamp, Positioning_preference, Positioning_feedback
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
rotation_OH <- read_csv("final_rotation_OH.csv")
## Rows: 12 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): Timestamp, Mentally_demanding, Physically_demanding
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
rotation_TH <- read_csv("final_rotation_TH.csv")
## Rows: 12 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): Timestamp, Mentally_demanding, Physically_demanding
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
rotation_preference <- read_csv("final_rotation_comparison.csv")
## Rows: 12 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): Timestamp, Rotation_preference, Rotation_feedback
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
scale_MG <- read_csv("final_scale_MG.csv")
## Rows: 12 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): Timestamp, Mentally_demanding, Physically_demanding
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
scale_NMG <- read_csv("final_scale_NMG.csv")
## Rows: 12 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): Timestamp, Mentally_demanding, Physically_demanding
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
scale_preference <- read_csv("final_scale_comparison.csv")
## Rows: 12 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): Timestamp, Scale_preference, Scale_feedback
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
combined_preference <- read_csv("final_outro_comparison.csv")
## Rows: 12 Columns: 6
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (5): Timestamp, Combined_positioning_preference, Combined_rotation_prefe...
## dbl (1): UserID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
summary(data)
##      UserID          TaskID            ActionID        
##  Min.   : 1.000   Length:49244       Length:49244      
##  1st Qu.: 4.000   Class :character   Class :character  
##  Median : 7.000   Mode  :character   Mode  :character  
##  Mean   : 6.741                                        
##  3rd Qu.:10.000                                        
##  Max.   :12.000                                        
##  rotateGlobeWhileDragging oneHandedRotationGesture moveGlobeWhileScaling
##  Mode :logical            Mode :logical            Mode :logical        
##  FALSE:36803              FALSE:11933              FALSE:46552          
##  TRUE :12441              TRUE :37311              TRUE :2692           
##                                                                         
##                                                                         
##                                                                         
##    distance          direction          complexity        zoomDirection     
##  Length:49244       Length:49244       Length:49244       Length:49244      
##  Class :character   Class :character   Class :character   Class :character  
##  Mode  :character   Mode  :character   Mode  :character   Mode  :character  
##                                                                             
##                                                                             
##                                                                             
##       Date                            Type           ActionStatus      
##  Min.   :2025-04-23 05:27:13.00   Length:49244       Length:49244      
##  1st Qu.:2025-04-25 01:36:58.00   Class :character   Class :character  
##  Median :2025-04-26 00:45:01.00   Mode  :character   Mode  :character  
##  Mean   :2025-04-27 21:46:53.98                                        
##  3rd Qu.:2025-05-01 07:26:51.00                                        
##  Max.   :2025-05-05 23:37:33.00                                        
##  main_translation_x  main_translation_y main_translation_z main_rotation_x   
##  Min.   :-7.099065   Min.   :-0.3298    Min.   :-3.487     Min.   :-0.97540  
##  1st Qu.:-0.400000   1st Qu.: 0.9000    1st Qu.:-1.921     1st Qu.:-0.03161  
##  Median :-0.004060   Median : 0.9000    Median :-1.500     Median : 0.00000  
##  Mean   :-0.005048   Mean   : 1.2326    Mean   :-1.683     Mean   :-0.03896  
##  3rd Qu.: 0.400000   3rd Qu.: 1.5539    3rd Qu.:-1.500     3rd Qu.: 0.00000  
##  Max.   : 3.256168   Max.   : 3.8304    Max.   : 5.006     Max.   : 0.97834  
##  main_rotation_y   main_rotation_z    main_rotation_w       main_scale_x    
##  Min.   :-1.0000   Min.   :-0.97710   Min.   :-0.9997261   Min.   :0.08431  
##  1st Qu.:-0.2033   1st Qu.: 0.00000   1st Qu.: 0.0000001   1st Qu.:0.99989  
##  Median : 0.9601   Median : 0.00000   Median : 0.0626987   Median :1.00000  
##  Mean   : 0.5003   Mean   : 0.01287   Mean   : 0.2756917   Mean   :0.99575  
##  3rd Qu.: 1.0000   3rd Qu.: 0.00000   3rd Qu.: 0.6346812   3rd Qu.:1.00002  
##  Max.   : 1.0000   Max.   : 0.98922   Max.   : 0.9999814   Max.   :7.69231  
##   main_scale_y      main_scale_z     target_translation_x target_translation_y
##  Min.   :0.08431   Min.   :0.08431   Min.   :-3.10000     Min.   :0.613       
##  1st Qu.:0.99994   1st Qu.:0.99990   1st Qu.:-0.40000     1st Qu.:0.900       
##  Median :1.00000   Median :1.00000   Median : 0.00000     Median :0.900       
##  Mean   :0.99577   Mean   :0.99576   Mean   :-0.02449     Mean   :1.245       
##  3rd Qu.:1.00002   3rd Qu.:1.00002   3rd Qu.: 0.40000     3rd Qu.:1.773       
##  Max.   :7.69231   Max.   :7.69231   Max.   : 2.33777     Max.   :2.547       
##  target_translation_z target_rotation_x target_rotation_y target_rotation_z 
##  Min.   :-3.3210      Min.   :-0.3928   Min.   :-0.6935   Min.   :-0.21194  
##  1st Qu.:-1.9598      1st Qu.:-0.3584   1st Qu.:-0.5655   1st Qu.: 0.00000  
##  Median :-1.5000      Median : 0.0000   Median : 1.0000   Median : 0.00000  
##  Mean   :-1.6971      Mean   :-0.1153   Mean   : 0.3768   Mean   :-0.01644  
##  3rd Qu.:-1.5000      3rd Qu.: 0.0000   3rd Qu.: 1.0000   3rd Qu.: 0.00000  
##  Max.   :-0.8953      Max.   : 0.0000   Max.   : 1.0000   Max.   : 0.13795  
##  target_rotation_w    target_scale_x   target_scale_y   target_scale_z  
##  Min.   :-0.9761015   Min.   :0.1700   Min.   :0.1700   Min.   :0.1700  
##  1st Qu.: 0.0000001   1st Qu.:1.0000   1st Qu.:1.0000   1st Qu.:1.0000  
##  Median : 0.0000001   Median :1.0000   Median :1.0000   Median :1.0000  
##  Mean   : 0.2914215   Mean   :0.9946   Mean   :0.9946   Mean   :0.9946  
##  3rd Qu.: 0.7119398   3rd Qu.:1.0000   3rd Qu.:1.0000   3rd Qu.:1.0000  
##  Max.   : 0.9807853   Max.   :2.0000   Max.   :2.0000   Max.   :2.0000  
##  match_accuracy_result    status         
##  Min.   : 0.00000      Length:49244      
##  1st Qu.: 0.00000      Class :character  
##  Median : 0.00000      Mode  :character  
##  Mean   : 0.03784                        
##  3rd Qu.: 0.00000                        
##  Max.   :22.31002
summary(demographic)
##      UserID       Timestamp         Academic_level        Gender         
##  Min.   : 1.00   Length:12          Length:12          Length:12         
##  1st Qu.: 3.75   Class :character   Class :character   Class :character  
##  Median : 6.50   Mode  :character   Mode  :character   Mode  :character  
##  Mean   : 6.50                                                           
##  3rd Qu.: 9.25                                                           
##  Max.   :12.00                                                           
##   Age_group           Exp_ARVR         Globe_usage_frequency
##  Length:12          Length:12          Length:12            
##  Class :character   Class :character   Class :character     
##  Mode  :character   Mode  :character   Mode  :character     
##                                                             
##                                                             
##                                                             
##  Have_used_VisionPro
##  Length:12          
##  Class :character   
##  Mode  :character   
##                     
##                     
## 
summary(positioning_NRG)
##      UserID       Timestamp         Mentally_demanding Physically_demanding
##  Min.   : 1.00   Length:12          Length:12          Length:12           
##  1st Qu.: 3.75   Class :character   Class :character   Class :character    
##  Median : 6.50   Mode  :character   Mode  :character   Mode  :character    
##  Mean   : 6.50                                                             
##  3rd Qu.: 9.25                                                             
##  Max.   :12.00
summary(positioning_RG)
##      UserID       Timestamp         Mentally_demanding Physically_demanding
##  Min.   : 1.00   Length:12          Length:12          Length:12           
##  1st Qu.: 3.75   Class :character   Class :character   Class :character    
##  Median : 6.50   Mode  :character   Mode  :character   Mode  :character    
##  Mean   : 6.50                                                             
##  3rd Qu.: 9.25                                                             
##  Max.   :12.00
summary(positioning_preference)
##      UserID       Timestamp         Positioning_preference Positioning_feedback
##  Min.   : 1.00   Length:12          Length:12              Length:12           
##  1st Qu.: 3.75   Class :character   Class :character       Class :character    
##  Median : 6.50   Mode  :character   Mode  :character       Mode  :character    
##  Mean   : 6.50                                                                 
##  3rd Qu.: 9.25                                                                 
##  Max.   :12.00
summary(rotation_OH)
##      UserID       Timestamp         Mentally_demanding Physically_demanding
##  Min.   : 1.00   Length:12          Length:12          Length:12           
##  1st Qu.: 3.75   Class :character   Class :character   Class :character    
##  Median : 6.50   Mode  :character   Mode  :character   Mode  :character    
##  Mean   : 6.50                                                             
##  3rd Qu.: 9.25                                                             
##  Max.   :12.00
summary(rotation_TH)
##      UserID       Timestamp         Mentally_demanding Physically_demanding
##  Min.   : 1.00   Length:12          Length:12          Length:12           
##  1st Qu.: 3.75   Class :character   Class :character   Class :character    
##  Median : 6.50   Mode  :character   Mode  :character   Mode  :character    
##  Mean   : 6.50                                                             
##  3rd Qu.: 9.25                                                             
##  Max.   :12.00
summary(rotation_preference)
##      UserID       Timestamp         Rotation_preference Rotation_feedback 
##  Min.   : 1.00   Length:12          Length:12           Length:12         
##  1st Qu.: 3.75   Class :character   Class :character    Class :character  
##  Median : 6.50   Mode  :character   Mode  :character    Mode  :character  
##  Mean   : 6.50                                                            
##  3rd Qu.: 9.25                                                            
##  Max.   :12.00
summary(scale_MG)
##      UserID       Timestamp         Mentally_demanding Physically_demanding
##  Min.   : 1.00   Length:12          Length:12          Length:12           
##  1st Qu.: 3.75   Class :character   Class :character   Class :character    
##  Median : 6.50   Mode  :character   Mode  :character   Mode  :character    
##  Mean   : 6.50                                                             
##  3rd Qu.: 9.25                                                             
##  Max.   :12.00
summary(scale_NMG)
##      UserID       Timestamp         Mentally_demanding Physically_demanding
##  Min.   : 1.00   Length:12          Length:12          Length:12           
##  1st Qu.: 3.75   Class :character   Class :character   Class :character    
##  Median : 6.50   Mode  :character   Mode  :character   Mode  :character    
##  Mean   : 6.50                                                             
##  3rd Qu.: 9.25                                                             
##  Max.   :12.00
summary(scale_preference)
##      UserID       Timestamp         Scale_preference   Scale_feedback    
##  Min.   : 1.00   Length:12          Length:12          Length:12         
##  1st Qu.: 3.75   Class :character   Class :character   Class :character  
##  Median : 6.50   Mode  :character   Mode  :character   Mode  :character  
##  Mean   : 6.50                                                           
##  3rd Qu.: 9.25                                                           
##  Max.   :12.00
summary(combined_preference)
##      UserID       Timestamp         Combined_positioning_preference
##  Min.   : 1.00   Length:12          Length:12                      
##  1st Qu.: 3.75   Class :character   Class :character               
##  Median : 6.50   Mode  :character   Mode  :character               
##  Mean   : 6.50                                                     
##  3rd Qu.: 9.25                                                     
##  Max.   :12.00                                                     
##  Combined_rotation_preference Combined_scale_preference Combined_feedback 
##  Length:12                    Length:12                 Length:12         
##  Class :character             Class :character          Class :character  
##  Mode  :character             Mode  :character          Mode  :character  
##                                                                           
##                                                                           
## 

Participants Demographic Information

# Total number of participants
length(unique(data$UserID))
## [1] 12
# Participants' gender distribution
demographic.gender <-  demographic %>%
  select(UserID, Gender) %>%
  distinct() %>%
  group_by(Gender) %>%
  summarise(count = n()) %>%
  mutate(percentage = round(count / sum(count) * 100, 1), percentage = paste0(percentage, "%"))

demographic.gender
## # A tibble: 2 × 3
##   Gender count percentage
##   <chr>  <int> <chr>     
## 1 Man       10 83.3%     
## 2 Woman      2 16.7%
# Participants' gender distribution chart
ggplot(demographic.gender, aes(x = "", y = count, fill = Gender)) +
  geom_col(width = 1, color = "white") +
  coord_polar(theta = "y") +
  geom_text(aes(label = percentage), position = position_stack(vjust = 0.5), size = 4) +
  labs(title = "Distribution of Participants' Gender") +
  theme_void()

# Participants' academic level distribution
demographic.academic_level <-  demographic %>%
  select(UserID, Academic_level) %>%
  distinct() %>%
  group_by(Academic_level) %>%
  summarise(count = n()) %>%
  mutate(percentage = round(count / sum(count) * 100, 1), graph_label = paste0(percentage, "%")) %>%
  rename(`Academic levels` = Academic_level)

demographic.academic_level
## # A tibble: 3 × 4
##   `Academic levels`       count percentage graph_label
##   <chr>                   <int>      <dbl> <chr>      
## 1 Graduate Student           10       83.3 83.3%      
## 2 Postdoctoral Researcher     1        8.3 8.3%       
## 3 Undergraduate Student       1        8.3 8.3%
# Participants' academic level distribution chart
ggplot(demographic.academic_level, aes(x = "", y = count, fill = `Academic levels`)) +
  geom_col(width = 1, color = "white") +
  coord_polar(theta = "y") +
  geom_text(aes(label = graph_label), position = position_stack(vjust = 0.5), size = 4) +
  labs(title = "Distribution of Participants' Academic Level") +
  theme_void() 

# Participants' previous AR/VR experience distribution
demographic.ARVR_exp <-  demographic %>%
  select(UserID, Exp_ARVR ) %>%
  distinct() %>%
  group_by(Exp_ARVR) %>%
  summarise(count = n()) %>%
  mutate(percentage = round(count / sum(count) * 100, 1), 
         label = paste0(percentage, "%"),
         ShortLabel = fct_recode(Exp_ARVR,
                          "No experience" = "I have no experience")
) %>%
  rename(`Previous AR/VR experience` = ShortLabel)

demographic.ARVR_exp
## # A tibble: 3 × 5
##   Exp_ARVR                         count percentage label Previous AR/VR exper…¹
##   <chr>                            <int>      <dbl> <chr> <fct>                 
## 1 Beginner (less than 5 hours exp…     4       33.3 33.3% Beginner (less than 5…
## 2 Familiar (5-20 hours experience)     3       25   25%   Familiar (5-20 hours …
## 3 I have no experience                 5       41.7 41.7% No experience         
## # ℹ abbreviated name: ¹​`Previous AR/VR experience`
# Participants' previous AR/VR experience distribution chart
ggplot(demographic.ARVR_exp, aes(x = "", y = count, fill = `Previous AR/VR experience`)) +
  geom_col(width = 1, color = "white") +
  coord_polar(theta = "y") +
  geom_text(aes(label = label), position = position_stack(vjust = 0.5), size = 4) +
  labs(title = "Distribution of Participants Previous AR/VR Experience") +
  theme_void() 

# Participants' previous globe experience distribution
demographic.globes_exp <- demographic %>%
  select(UserID, Globe_usage_frequency) %>%
  distinct() %>%
  group_by(Globe_usage_frequency) %>%
  summarise(count = n()) %>%
  mutate(percentage = round(count / sum(count) * 100, 1),
         graph_label = paste0(percentage, "%")) %>%
  rename(`Previous globes experience` = Globe_usage_frequency)

demographic.globes_exp
## # A tibble: 3 × 4
##   `Previous globes experience` count percentage graph_label
##   <chr>                        <int>      <dbl> <chr>      
## 1 A few times a month              1        8.3 8.3%       
## 2 A few times a year               3       25   25%        
## 3 Once every few years             8       66.7 66.7%
# Participants' previous globe experience distribution chart
ggplot(demographic.globes_exp, aes(x = "", y = count, fill = `Previous globes experience`)) +
  geom_col(width = 1, color = "white") +
  coord_polar(theta = "y") +
  geom_text(aes(label = graph_label), position = position_stack(vjust = 0.5), size = 4) +
  labs(title = "Distribution of Participants Previous AR/VR Experience") +
  theme_void() 

# Participants' previous Apple Vision Pro Experience distribution
demographic.visionpro_exp <- demographic %>%
  select(UserID, Have_used_VisionPro) %>%
  distinct() %>%
  group_by(Have_used_VisionPro) %>%
  summarise(count = n()) %>%
  mutate(
    percentage = round(count / sum(count) * 100, 1),
    graph_label = paste0(percentage, "%")
  ) %>%
  rename(`Have used Apple Vision Pro` = Have_used_VisionPro)
  
demographic.visionpro_exp
## # A tibble: 2 × 4
##   `Have used Apple Vision Pro`                   count percentage graph_label
##   <chr>                                          <int>      <dbl> <chr>      
## 1 I have never used the Apple Vision Pro            11       91.7 91.7%      
## 2 I have used the Apple Vision Pro once or twice     1        8.3 8.3%
# Participants' previous Apple Vision Pro Experience distribution chart
ggplot(demographic.visionpro_exp, aes(x = "", y = count, fill = `Have used Apple Vision Pro`)) +
  geom_col(width = 1, color = "white") +
  coord_polar(theta = "y") +
  geom_text(aes(label = graph_label), position = position_stack(vjust = 0.5), size = 4) +
  labs(title = "Distribution of Participants Previous AR/VR Experience") +
  theme_void()   

User Study

Study: Positioning

Positioning Data Preparation

data.positioning <- data %>%
  mutate(positionCondition = if_else(rotateGlobeWhileDragging, "rotatingGlobe", "nonRotatingGlobe")) %>%
  select(-rotateGlobeWhileDragging) %>%
  inner_join(demographic, by = "UserID") %>%
  inner_join(positioning_NRG, by = "UserID") %>%
  rename(
    PAAS_NRG = Mentally_demanding,
    BORG_NRG = Physically_demanding
  ) %>%
  mutate(
    PAAS_NRG = as.numeric(str_extract(PAAS_NRG, "\\d+(\\.\\d+)?")),
    BORG_NRG = as.numeric(str_extract(BORG_NRG, "\\d+(\\.\\d+)?"))
  ) %>%
  mutate(
    PAAS_NRG = if_else(positionCondition == "nonRotatingGlobe", PAAS_NRG, NA_real_),
    BORG_NRG = if_else(positionCondition == "nonRotatingGlobe", BORG_NRG, NA_real_)
  ) %>%
  inner_join(positioning_RG, by = "UserID") %>%
  rename(
    PAAS_RG = Mentally_demanding,
    BORG_RG = Physically_demanding
  ) %>%
  mutate(
    PAAS_RG = as.numeric(str_extract(PAAS_RG, "\\d+(\\.\\d+)?")),
    BORG_RG = as.numeric(str_extract(BORG_RG, "\\d+(\\.\\d+)?"))
  ) %>%
  mutate(
    PAAS_RG = if_else(positionCondition == "rotatingGlobe", PAAS_RG, NA_real_),
    BORG_RG = if_else(positionCondition == "rotatingGlobe", BORG_RG, NA_real_)
  ) %>%
  inner_join(positioning_preference, by = "UserID") %>%
  rename(
    behaviour_preference = Positioning_preference,
    behaviour_feedback = Positioning_feedback
  ) %>%
  mutate(
      behaviour_preference = case_when(
    str_detect(behaviour_preference, "Static orientation") ~ "staticOrientation",
    str_detect(behaviour_preference, "Adaptive orientation") ~ "adaptiveOrientation",
    str_detect(behaviour_preference, "no preference") ~ "noPreference",
    TRUE ~ "unknown"
  ) ) %>%
  filter(Type == "positionTask") %>%
  select(UserID, TaskID, ActionID, positionCondition, distance, direction, Date, ActionStatus, main_translation_x,
  main_translation_y, main_translation_z, target_translation_x, target_translation_y, target_translation_z, 
  match_accuracy_result, status, PAAS_NRG, BORG_NRG, PAAS_RG, BORG_RG, behaviour_preference, behaviour_feedback) %>%
  mutate(distance = as.factor(distance), 
         direction = as.factor(direction), 
         positionCondition = as.factor(positionCondition),
         status = as.factor(status),
         behaviour_preference = as.factor(behaviour_preference))

Position Task Study

Accuracy
Normality
data.positioning.matched <- data.positioning %>%
  filter(status == "Matched")

shapiro.test(data.positioning.matched$match_accuracy_result)
## 
##  Shapiro-Wilk normality test
## 
## data:  data.positioning.matched$match_accuracy_result
## W = 0.97029, p-value = 2.086e-09
hist(data.positioning.matched$match_accuracy_result, breaks = 100,
     main = "Histogram (Zoomed)", xlab = "Accuracy",
     col = "lightblue", xlim = c(0, 0.06))

plot(density(data.positioning.matched$match_accuracy_result), 
     main = "Density Plot (Zoomed)", xlab = "Accuracy",
     col = "blue", lwd = 2, xlim = c(0, 0.6))

Although the w value is close to 1, the p value is below 0.05 so we reject null hypothesis that the data is normally distributed So, we cannot use one way ANOVA, instead, we use Wilcoxon signed-rank test

Statistical Tests
data.positioning.matched.accuracy_avg.long <- data.positioning.matched %>%
  group_by(UserID, positionCondition, distance, direction) %>%
  summarise(mean_accuracy = mean(match_accuracy_result, na.rm = TRUE), .groups = 'drop')

data.positioning.matched.art <- art(mean_accuracy ~ positionCondition * distance * direction + (1|UserID), data = data.positioning.matched.accuracy_avg.long)

anova(data.positioning.matched.art)
## Analysis of Variance of Aligned Rank Transformed Data
## 
## Table Type: Analysis of Deviance Table (Type III Wald F tests with Kenward-Roger df) 
## Model: Mixed Effects (lmer)
## Response: art(mean_accuracy)
## 
##                                              F Df Df.res    Pr(>F)   
## 1 positionCondition                    0.88851  1 99.000 0.3481756   
## 2 distance                             8.58240  1 99.545 0.0042088 **
## 3 direction                            3.51399  2 99.527 0.0335248  *
## 4 positionCondition:distance           0.88433  1 99.000 0.3493079   
## 5 positionCondition:direction          0.21943  2 99.000 0.8033694   
## 6 distance:direction                   0.25114  2 99.512 0.7784052   
## 7 positionCondition:distance:direction 1.62425  2 99.000 0.2022653   
## ---
## Signif. codes:   0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    # • The main effect of positionCondition was not statistically significant: F(1, 99) = 0.89, p = 0.348.
    # • The main effect of distance was statistically significant: F(1, 99.55) = 8.58, p = 0.0042 (**).
    # • The main effect of direction was also statistically significant: F(2, 99.53) = 3.51, p = 0.0335 (*).
    # • The interaction between positionCondition and distance was not significant: F(1, 99) = 0.88, p = 0.349.
    # • The interaction between positionCondition and direction was not significant: F(2, 99) = 0.22, p = 0.803.
    # • The interaction between distance and direction was not significant: F(2, 99.51) = 0.25, p = 0.778.
    # • The three-way interaction (positionCondition × distance × direction) was not significant: F(2, 99) = 1.62, p = 0.202.

# There were significant main effects of both distance and direction on mean accuracy, indicating that participants’ accuracy was influenced independently by how far the task was and from which direction it came. However, positionCondition had no significant effect, and no interaction terms reached statistical significance. This suggests that the combined effects of position, distance, and direction do not differentially impact accuracy beyond the main effects of distance and direction alone.

ggplot(data.positioning.matched.accuracy_avg.long, aes(x = positionCondition, y = mean_accuracy, group = UserID)) +
  geom_line(aes(color = as.factor(UserID)),  linewidth = 1, alpha = 0.6) +
  geom_point(size = 3) +
  facet_wrap(~ distance + direction) + 
  labs(title = "Paired Accuracy by Globe Behaviour, Globes' Distance and Direction",
       x = "Behaviour",
       y = "Match Accuracy") +
  theme_minimal() +
  theme(strip.text = element_text(size = 12, face = "bold"))

ggplot(data.positioning.matched.accuracy_avg.long, aes(x = positionCondition, y = mean_accuracy)) +
  geom_boxplot(outlier.shape = NA, fill = "lightblue") +
  geom_jitter(width = 0.1, size = 2, alpha = 0.7) +
  facet_wrap(~ distance + direction) + 
  labs(title = "Boxplots of Accuracy by Globe Behaviour, Globes' Distance and Direction",
       x = "Behaviour",
       y = "Match Accuracy") +
  theme_minimal()

Completion Time
Normality
data.positioning.taskCompletion_avg <- data.positioning %>%
  group_by(UserID, TaskID) %>%
  summarise(
    completion_time = as.numeric(difftime(max(Date), min(Date), units = "mins")),
    .groups = "drop"
  )

shapiro.test(data.positioning.taskCompletion_avg$completion_time)
## 
##  Shapiro-Wilk normality test
## 
## data:  data.positioning.taskCompletion_avg$completion_time
## W = 0.59479, p-value < 2.2e-16
Statistical Tests
data.positioning.taskCompletion_avg.long <- data.positioning %>%
  group_by(UserID, positionCondition, TaskID, distance, direction) %>%
  summarise(
    completion_time = as.numeric(difftime(max(Date), min(Date), units = "mins")),
    .groups = "drop"
  ) %>%
  group_by(UserID, positionCondition, distance, direction) %>%
  summarise(
    avg_completion_time = mean(completion_time),
    .groups = "drop"
  )

data.positioning.taskCompletion_avg.art <- art(avg_completion_time ~ positionCondition * distance * direction + (1|UserID), data = data.positioning.taskCompletion_avg.long)

anova(data.positioning.taskCompletion_avg.art)
## Analysis of Variance of Aligned Rank Transformed Data
## 
## Table Type: Analysis of Deviance Table (Type III Wald F tests with Kenward-Roger df) 
## Model: Mixed Effects (lmer)
## Response: art(avg_completion_time)
## 
##                                              F Df  Df.res    Pr(>F)   
## 1 positionCondition                    0.61363  1  99.002 0.4352944   
## 2 distance                             1.75116  1 100.619 0.1887297   
## 3 direction                            6.38814  2 100.863 0.0024424 **
## 4 positionCondition:distance           0.24125  1  99.002 0.6243926   
## 5 positionCondition:direction          0.50478  2  99.002 0.6051833   
## 6 distance:direction                   0.57384  2 100.707 0.5651868   
## 7 positionCondition:distance:direction 0.43726  2  99.002 0.6470467   
## ---
## Signif. codes:   0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# • positionCondition had no significant effect on average task completion time:F(1, 99.00) = 0.61, p = 0.435
# • distance also showed no significant effect:F(1, 100.62) = 1.75, p = 0.189
# • direction showed a statistically significant main effect:F(2, 100.86) = 6.39, p = 0.0024 (**), suggesting direction influences how long tasks take
# • The interaction between positionCondition and distance was not significant:F(1, 99.00) = 0.24, p = 0.624
# • The interaction between positionCondition and direction was not significant:F(2, 99.00) = 0.50, p = 0.605
# • The interaction between distance and direction was not significant:F(2, 100.71) = 0.57, p = 0.565
# • The three-way interaction (positionCondition × distance × direction) was also not significant:F(2, 99.00) = 0.44, p = 0.647
# 
# Only direction had a statistically significant effect on average task completion time, indicating that the direction from which the task was approached meaningfully influenced how long participants took to complete it. Other factors—positionCondition, distance, and all interaction terms—did not have significant effects. This suggests that regardless of position or distance, direction alone may account for differences in task completion time in this context.

ggplot(data.positioning.taskCompletion_avg.long, aes(x = positionCondition, y = avg_completion_time, group = UserID)) +
  geom_line(aes(color = as.factor(UserID)), linewidth = 1, alpha = 0.6) +
  geom_point(size = 3) +
  facet_wrap(~ distance + direction) + 
  labs(
    title = "Paired Task Completion Time by Behaviour, Distance, and Direction",
    x = "Behaviour",
    y = "Completion Time (minutes)",
    color = "UserID"
  ) +
  theme_minimal()

ggplot(data.positioning.taskCompletion_avg.long, aes(x = positionCondition, y = avg_completion_time)) +
  geom_boxplot(outlier.shape = NA, fill = "lightblue") +
  geom_jitter(width = 0.1, size = 2, alpha = 0.7) +
  facet_wrap(~ distance + direction) + 
  labs(title = "Boxplots of Task Completion Time by Behaviour, Distance, and Directio",
       x = "Behaviour",
       y = "Task Completion Time") +
  theme_minimal()

Subjective Measures
Physical and Mental Exertion
data.positioning.long <- data.positioning %>% 
  select(UserID, positionCondition, PAAS_RG, BORG_RG, PAAS_NRG, BORG_NRG) %>%
  distinct() %>%
  pivot_longer(
    cols = c(PAAS_RG, BORG_RG, PAAS_NRG, BORG_NRG),
    names_to = "Measure",
    values_to = "Score"
  ) %>%
  filter(!is.na(Score)) %>%  
  mutate(
    ExertionType = case_when(
      str_detect(Measure, "PAAS") ~ "Cognitive load",
      str_detect(Measure, "BORG") ~ "Physical exertion",
      TRUE ~ "Unknown"
    ),
    UserID = as.factor(UserID),
    ExertionType = as.factor(ExertionType)
  ) %>%
  group_by(UserID, positionCondition, Measure)

data.positioning.long.art_anova <- art(Score ~ positionCondition * ExertionType + (1|UserID), data = data.positioning.long)
anova(data.positioning.long.art_anova)
## Analysis of Variance of Aligned Rank Transformed Data
## 
## Table Type: Analysis of Deviance Table (Type III Wald F tests with Kenward-Roger df) 
## Model: Mixed Effects (lmer)
## Response: art(Score)
## 
##                                         F Df Df.res    Pr(>F)   
## 1 positionCondition              12.54732  1     33 0.0012077 **
## 2 ExertionType                   12.80843  1     33 0.0010921 **
## 3 positionCondition:ExertionType  0.17619  1     33 0.6773922   
## ---
## Signif. codes:   0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#   •   Main effect of positionCondition: F(1, 33) = 12.55, p = 0.0012 This is statistically significant, indicating that task positioning condition (e.g., rotating vs non-rotating globe) has a significant effect on the overall exertion score (which includes cognitive load and physical exertion).
#   •   Main effect of ExertionType: F(1, 33) = 12.81, p = 0.0011 Also statistically significant, meaning that there is a clear difference between cognitive load (PAAS) and physical exertion (BORG) scores regardless of the positioning condition.
#   •   Interaction effect (positionCondition × ExertionType): F(1, 33) = 0.18, p = 0.6774, suggesting that the effect of positioning condition does not depend on the type of exertion, and vice versa. The differences in PAAS and BORG scores are consistent across both positioning conditions.
# Both the positioning condition and type of exertion significantly influence the exertion score independently, but there is no interaction effect. This means that while both factors matter, their effects do not amplify or diminish each other. For example, rotating vs non-rotating globes affects scores, and cognitive vs physical exertion shows distinct levels, but the difference between PAAS and BORG scores remains similar across positioning conditions.

ggplot(data.positioning.long, aes(x = positionCondition, y = Score, fill = ExertionType)) +
  geom_boxplot(width = 0.1, position = position_dodge(0.8)) +
  labs(
    x = "Position Condition",
    y = "Score",
    title = "Distribution of Scores by Behaviour and Exertion Type"
  ) +
  theme_minimal()

Preference
data.positioning %>%
  select(UserID, behaviour_preference) %>%
  distinct() %>%  
  count(behaviour_preference) %>%
  mutate(
    percent = n / sum(n),
    ncount = paste0(n, "\n", percent_format()(percent))
  ) %>%
  ggplot(aes(x = "", y = n, fill = behaviour_preference)) +
  geom_col(width = 1, color = "white") +
  coord_polar(theta = "y") +
  geom_text(aes(label = ncount), position = position_stack(vjust = 0.5), size = 4) +
  labs(
    title = "Distribution of Positioning Behaviour Preferences",
    fill = "Preference"
  ) +
  theme_void()

Comments
data.positioning.preference.summary <- data.positioning %>%
  mutate(
    behaviour_preference = case_when(
      behaviour_preference == "staticOrientation" ~ "Static Orientation",
      behaviour_preference == "adaptiveOrientation" ~ "Adaptive Orientation",
      behaviour_preference == "noPreference" ~ "No Preference",
      TRUE ~ behaviour_preference
    )
  ) %>%
  group_by(UserID) %>%
  summarise(
    behaviour_preference = first(behaviour_preference),
    behaviour_feedback = first(behaviour_feedback),
    .groups = "drop"
  )

kable(data.positioning.preference.summary, caption = "User Feedback Summary - Positioning")
User Feedback Summary - Positioning
UserID behaviour_preference behaviour_feedback
1 Static Orientation I prefer the static orientation as it makes me feel more enjoyable and easy to move it. However, in relation to moving the globe, I think, gaze is effective enough but the pinch gesture must be changed into other gestures such as thumb movement.
2 Adaptive Orientation Static orientation give me a little bit of nausea. And regarding the control, the x and y axis gesture is easy to control, but for both negative and positive z-axis is a bit hard to do since its depends on my hand’s position and hand’s length.
3 Static Orientation Static is more intuitove because it only display 1 type of direction to control than adaptive. In order to rotate the globe, i suggest to introduce one more gesture where we can pinch and rotate finger at the same time.
4 Static Orientation I like it when it static it is more dynamic and realistic like a globe should be, to move the globe I think it would be better if we can pinch and throw the globe to the designated positions. or maybe we can move the globe by the palm of our hands.
5 Adaptive Orientation It’s easier to see the same side and of the earth. It will be better if I can grab it like a real globe
6 Static Orientation I prefer the static one because it more realistic, more natural.its more convenient to observe, and it feels like we use real globe. I feel the gaze and pinch method is better that’s directly touch, it is also less prone to errors.
7 Adaptive Orientation I prefer the adaptive orientation one because it remains focused and detailed.
8 Static Orientation The static one feels more real like physical globe. It would be convinient if there is a frame like a physical globe where we can move the globe around with that.
9 Static Orientation I prefer the static one because its more intuitive
10 Adaptive Orientation I prefer adaptive one because its easier to observe the surface.
11 Static Orientation I prefer static because its easier to focus on the globe and less confusing
12 No Preference 1. It depends on the situation, if the situation doesn’t require me to actually show the globe to other people, I wouldn’t mind if it doesn’t move. But, if it requires me to show other people (live presentation), I would want it to adaptively look towards me every time. Because if not I need to always adjust the orientation.
  1. The DPI needs to be adaptive, my preference is to pinch the globe and move it towards the destination. But, it always falls shorter than it should have been.
  2. I had difficulty in selecting the main globe when it is obstructed by the target globe. This is due to the need to repinch when my hand is out of space. |
Summary

Study: Rotating

Rotating Data Preparation

data.rotating <- data %>%
  mutate(rotationCondition = if_else(oneHandedRotationGesture, "oneHanded", "twoHanded")) %>%
  select(-oneHandedRotationGesture) %>%
  inner_join(demographic, by = "UserID") %>%
  inner_join(rotation_OH, by = "UserID") %>%
  rename(
    PAAS_OH = Mentally_demanding,
    BORG_OH = Physically_demanding
  ) %>%
  mutate(
    PAAS_OH = as.numeric(str_extract(PAAS_OH, "\\d+(\\.\\d+)?")),
    BORG_OH = as.numeric(str_extract(BORG_OH, "\\d+(\\.\\d+)?"))
  ) %>%
  mutate(
    PAAS_OH = if_else(rotationCondition == "oneHanded", PAAS_OH, NA_real_),
    BORG_OH = if_else(rotationCondition == "oneHanded", BORG_OH, NA_real_)
  ) %>%
  inner_join(rotation_TH, by = "UserID") %>%
  rename(
    PAAS_TH = Mentally_demanding,
    BORG_TH = Physically_demanding
  ) %>%
  mutate(
    PAAS_TH = as.numeric(str_extract(PAAS_TH, "\\d+(\\.\\d+)?")),
    BORG_TH = as.numeric(str_extract(BORG_TH, "\\d+(\\.\\d+)?"))
  ) %>%
  mutate(
    PAAS_TH = if_else(rotationCondition == "twoHanded", PAAS_TH, NA_real_),
    BORG_TH = if_else(rotationCondition == "twoHanded", BORG_TH, NA_real_)
  ) %>%
  inner_join(rotation_preference, by = "UserID") %>%
  rename(
    behaviour_preference = Rotation_preference,
    behaviour_feedback = Rotation_feedback
  ) %>%
  mutate(
      behaviour_preference = case_when(
    str_detect(behaviour_preference, "One-handed") ~ "oneHandedPreference",
    str_detect(behaviour_preference, "Two-handed") ~ "twoHandedPreference",
    str_detect(behaviour_preference, "no preference") ~ "noPreference",
    TRUE ~ "unknown"
  )) %>%
  filter(Type == "rotationTask") %>%
  select(UserID, TaskID, ActionID, rotationCondition, complexity, Date, ActionStatus, main_rotation_x,
  main_rotation_y, main_rotation_z, main_rotation_w, target_rotation_x, target_rotation_y, target_rotation_z,
  target_rotation_w,match_accuracy_result, status, PAAS_OH, BORG_OH, PAAS_TH, BORG_TH, behaviour_preference, behaviour_feedback) %>%
  mutate(complexity = as.factor(complexity), 
         rotationCondition = as.factor(rotationCondition),
         status = as.factor(status),
         behaviour_preference = as.factor(behaviour_preference))

Rotation Task Study

Accuracy
Normality
data.rotating.matched <- data.rotating %>%
  filter(status == "Matched")

shapiro.test(data.rotating.matched$match_accuracy_result)
## 
##  Shapiro-Wilk normality test
## 
## data:  data.rotating.matched$match_accuracy_result
## W = 0.94156, p-value = 5.023e-07
hist(data.rotating.matched$match_accuracy_result, breaks = 100,
     main = "Histogram (Zoomed)", xlab = "Accuracy",
     col = "lightblue", xlim = c(0, 0.5))

plot(density(data.rotating.matched$match_accuracy_result), 
     main = "Density Plot (Zoomed)", xlab = "Accuracy",
     col = "blue", lwd = 2, xlim = c(0, 0.5))

Although the w value is close to 1, the p value is below 0.05 so we reject null hypothesis that the data is normally distributed So, we cannot use one way ANOVA, instead, we use Wilcoxon signed-rank test

Statistical tests
data.rotating.matched.accuracy_avg.long <- data.rotating.matched %>%
  group_by(UserID, rotationCondition, complexity) %>%
  summarise(mean_accuracy = mean(match_accuracy_result, na.rm = TRUE), .groups = 'drop') 

data.rotating.matched.art <- art(mean_accuracy ~ rotationCondition * complexity + (1|UserID), data = data.rotating.matched.accuracy_avg.long)

anova(data.rotating.matched.art)
## boundary (singular) fit: see help('isSingular')
## boundary (singular) fit: see help('isSingular')
## boundary (singular) fit: see help('isSingular')
## Analysis of Variance of Aligned Rank Transformed Data
## 
## Table Type: Analysis of Deviance Table (Type III Wald F tests with Kenward-Roger df) 
## Model: Mixed Effects (lmer)
## Response: art(mean_accuracy)
## 
##                                       F Df Df.res     Pr(>F)    
## 1 rotationCondition             4.67385  1     33   0.037975   *
## 2 complexity                   20.74234  1     33 6.8141e-05 ***
## 3 rotationCondition:complexity  0.25022  1     33   0.620238    
## ---
## Signif. codes:   0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    # • rotationCondition: F(1, 33) = 4.67, p = 0.038 Significant: rotation affects accuracy
    # • complexity: F(1, 33) = 20.74, p < 0.001 Highly significant: complexity affects accuracy
    # • rotationCondition × complexity: F(1, 33) = 0.25, p = 0.62 Not significant: no interaction effect

# The ART ANOVA revealed significant main effects of both rotation condition and complexity on accuracy. Rotation condition had a modest but significant effect (p = 0.038), while complexity showed a strong influence (p < 0.001). However, there was no significant interaction between rotation and complexity (p = 0.62), indicating that the effects of each factor on accuracy are independent and do not influence each other.


ggplot(data.rotating.matched.accuracy_avg.long, aes(x = rotationCondition, y = mean_accuracy, group = UserID)) +
  geom_line(aes(color = as.factor(UserID)), linewidth = 1, alpha = 0.6) +
  geom_point(size = 3) +
  facet_wrap(~ complexity) + 
  labs(
    title = "Paired Task Match Accuracy by Technique and Complexity",
    x = "Technique",
    y = "Accuracy",
    color = "UserID"
  ) +
  theme_minimal() +
  theme(strip.text = element_text(size = 12, face = "bold"))

ggplot(data.rotating.matched.accuracy_avg.long, aes(x = rotationCondition, y = mean_accuracy)) +
  geom_boxplot(outlier.shape = NA, fill = "lightblue") +
  geom_jitter(width = 0.1, size = 2, alpha = 0.7) +
  facet_wrap(~ complexity) + 
  labs(title = "Boxplots of Task Accuracy by Technique and Complexity",
       x = "Technique",
       y = "Match Accuracy") +
  theme_minimal()

Completion Time
Normality
data.rotating.taskCompletion_avg <- data.rotating %>%
  group_by(UserID, TaskID) %>%
  summarise(
    completion_time = as.numeric(difftime(max(Date), min(Date), units = "mins")),
    .groups = "drop"
  )

shapiro.test(data.rotating.taskCompletion_avg$completion_time)
## 
##  Shapiro-Wilk normality test
## 
## data:  data.rotating.taskCompletion_avg$completion_time
## W = 0.49195, p-value < 2.2e-16
Statistical Tests
data.rotating.taskCompletion_avg.long <- data.rotating %>%
  group_by(UserID, rotationCondition, complexity, TaskID) %>%
  summarise(
    completion_time = as.numeric(difftime(max(Date), min(Date), units = "mins")),
    .groups = "drop"
  ) %>%
  group_by(UserID, rotationCondition, complexity) %>%
  summarise(
    avg_completion_time = mean(completion_time),
    .groups = "drop"
  )

data.rotating.taskCompletion_avg.art <- art(avg_completion_time ~ rotationCondition * complexity + (1|UserID), data = data.rotating.taskCompletion_avg.long)

anova(data.rotating.taskCompletion_avg.art)
## boundary (singular) fit: see help('isSingular')
## boundary (singular) fit: see help('isSingular')
## boundary (singular) fit: see help('isSingular')
## Analysis of Variance of Aligned Rank Transformed Data
## 
## Table Type: Analysis of Deviance Table (Type III Wald F tests with Kenward-Roger df) 
## Model: Mixed Effects (lmer)
## Response: art(avg_completion_time)
## 
##                                      F Df Df.res     Pr(>F)    
## 1 rotationCondition             8.4424  1     33   0.006499  **
## 2 complexity                   37.7528  1     33 6.3129e-07 ***
## 3 rotationCondition:complexity  2.3552  1     33   0.134399    
## ---
## Signif. codes:   0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
    # • rotationCondition: F(1, 33) = 8.44, p = 0.0065 (**) Statistically significant — rotation has a clear effect on completion time
    # • complexity: F(1, 33) = 37.75, p < 0.0001 (***) Statistically significant — complexity strongly affects completion time
    # • rotationCondition × complexity: F(1, 33) = 2.36, p = 0.1344 Not significant — no evidence of interaction between rotation and complexity

ggplot(data.rotating.taskCompletion_avg.long, 
       aes(x = rotationCondition, y = avg_completion_time, group = UserID)) +
  geom_line(aes(color = as.factor(UserID)), linewidth = 1, alpha = 0.6) +
  geom_point(size = 3) +
  facet_wrap(~ complexity) +
  labs(
    title = "Paired Task Completion Time by Technique and Complexity",
    x = "Technique",
    y = "Average Completion Time (minutes)",
    color = "UserID"
  ) +
  theme_minimal() +
  theme(strip.text = element_text(size = 12, face = "bold"))

ggplot(data.rotating.taskCompletion_avg.long, aes(x = rotationCondition, y = avg_completion_time)) +
  geom_boxplot(outlier.shape = NA, fill = "lightblue") +
  geom_jitter(width = 0.1, size = 2, alpha = 0.7) +
  facet_wrap(~ complexity) +
  labs(title = "Boxplots of Task Completion Time by Technique and Complexity",
       x = "Technique",
       y = "Task Completion Time") +
  theme_minimal()

Subjective Measures
Physical and Mental Exertion
data.rotating.long <- data.rotating %>% 
  select(UserID, rotationCondition, PAAS_OH, BORG_OH, PAAS_TH, BORG_TH) %>%
  distinct() %>%
  pivot_longer(
    cols = c(PAAS_OH, BORG_OH, PAAS_TH, BORG_TH),
    names_to = "Measure",
    values_to = "Score"
  ) %>%
  filter(!is.na(Score)) %>%  
  mutate(
    ExertionType = case_when(
      str_detect(Measure, "PAAS") ~ "Cognitive load",
      str_detect(Measure, "BORG") ~ "Physical exertion",
      TRUE ~ "Unknown"
    ),
    UserID = as.factor(UserID),
    ExertionType = as.factor(ExertionType)
  ) %>%
  group_by(UserID, rotationCondition, Measure)
  
data.rotating.long.art_anova <- art(Score ~ rotationCondition * ExertionType + (1|UserID), data = data.rotating.long)
anova(data.rotating.long.art_anova)
## Analysis of Variance of Aligned Rank Transformed Data
## 
## Table Type: Analysis of Deviance Table (Type III Wald F tests with Kenward-Roger df) 
## Model: Mixed Effects (lmer)
## Response: art(Score)
## 
##                                          F Df Df.res     Pr(>F)    
## 1 rotationCondition               4.276241  1     33   0.046561   *
## 2 ExertionType                   20.747724  1     33 6.8023e-05 ***
## 3 rotationCondition:ExertionType  0.016351  1     33   0.899028    
## ---
## Signif. codes:   0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#   •   Main effect of rotationCondition: F(1, 33) = 4.28, p = 0.0466 This is statistically significant at the 0.05 level, indicating that rotating vs non-rotating globe conditions have a meaningful impact on participants’ perceived exertion scores (cognitive or physical).
#     • Main effect of ExertionType: F(1, 33) = 20.75, p < 0.001 This is highly significant,  showing a strong difference between cognitive load (PAAS) and physical exertion (BORG) scores, regardless of the globe rotation condition.
#   •   Interaction effect (rotationCondition × ExertionType): F(1, 33) = 0.016, p = 0.8990 Not significant. Suggesting that the difference between cognitive and physical exertion is consistent across both rotation conditions. The type of exertion does not change depending on whether the globe was rotating or not.
# Both rotation condition and exertion type have independent effects on perceived exertion. Participants reported different exertion levels between rotating and non-rotating globes, and also between cognitive and physical demands. However, there is no interaction, meaning the relative difference between PAAS and BORG remains stable across rotation types.

ggplot(data.rotating.long, aes(x = rotationCondition, y = Score, fill = ExertionType)) +
  geom_boxplot(width = 0.1, position = position_dodge(0.8)) +
  labs(
    x = "Rotation Condition",
    y = "Score",
    title = "Distribution of Scores by Technique and Exertion Type"
  ) +
  theme_minimal()

Preference
data.rotating %>%
  select(UserID, behaviour_preference) %>%
  distinct() %>%  
  count(behaviour_preference) %>%
  mutate(
    percent = n / sum(n),
    ncount = paste0(n, "\n", percent_format()(percent))
  ) %>%
  ggplot(aes(x = "", y = n, fill = behaviour_preference)) +
  geom_col(width = 1, color = "white") +
  coord_polar(theta = "y") +
  geom_text(aes(label = ncount), position = position_stack(vjust = 0.5), size = 4) +
  labs(
    title = "Distribution of Rotation Behaviour Preferences",
    fill = "Preference"
  ) +
  theme_void()

Comments
data.rotating.preference.summary <- data.rotating %>%
  mutate(
    behaviour_preference = case_when(
      behaviour_preference == "oneHandedPreference" ~ "One Handed Gesture",
      behaviour_preference == "twoHandedPreference" ~ "Two Handed Gesture",
      behaviour_preference == "noPreference" ~ "No Preference",
      TRUE ~ behaviour_preference
    )
  ) %>%
  group_by(UserID) %>%
  summarise(
    behaviour_preference = first(behaviour_preference),
    behaviour_feedback = first(behaviour_feedback),
    .groups = "drop"
  )

kable(data.rotating.preference.summary, caption = "User Feedback Summary - Rotating")
User Feedback Summary - Rotating
UserID behaviour_preference behaviour_feedback
1 One Handed Gesture I feel more convenient to use one-handed rotation gesture because it is less confusing compared to two-handed rotation gesture, where I had a bit more difficulties in balancing my hands.
2 Two Handed Gesture I have more control with the two-handed rotation gesture, it feels more natural. But still feel limited In terms of flexibility upon rotation. I think more gesture such as moving the globe position when both hands are moving simultaneously following the centre of the hands.
3 One Handed Gesture More fingers means more calorie burns. But it has limitation with the control, not sure how to solve or give gesture recommendation.
4 One Handed Gesture I like one handed better because I have more control to rotate the orientations as I like, as for the gestures I ’ll suggest maybe we can use hands like waving gestures to rotate the globe
5 No Preference Both options have their advantages. One hand is simple but little bit harder for complex task like rotation. I think it would be better if I can rotate our pump like rotating door knob
6 One Handed Gesture I prefer one-handed gesture one because its easier to imagine the direction. However, the two-handed gesture will be useful in medical field. Especially in surgery.
7 One Handed Gesture I prefer one-handed gesture because it is handy and more flexible.
8 Two Handed Gesture I prefer two handed because it gives more flexibility. However I feel that two handed takes time to adapt. I think it would be better if we can touch and manipulate directly like aphysical globe. If the globe is far, we can use gaze and pinch to make it nearer, then we can use direct gesture manipulation.
9 One Handed Gesture I prefer one handed because it is simpler.
10 One Handed Gesture I prefer one handed because it is easier to move the globe from any directions while the two handed it is more difficult because it takes two-hands coordination. Gaze and pinch is convinient enough.
11 One Handed Gesture I prefer one handed because thats how I usually operate globe in real life. Unless the two handed uses palms like holding real globes, I’d prefer it.
12 No Preference 1. It would be better if we have the option of using two hands, instead of directly using 2 hands. It gives the option of z axis adjustment in the middle of x,y axis rotation.
  1. It would be better if we could rotate it with our palm like in iron man 2.
  2. It would be confusing when you try to combine both rotation and position with the same gestures. |
Summary

Study: Scale

Scale Data Preparation

data.scale <- data %>%
  mutate(scaleCondition = if_else(moveGlobeWhileScaling, "movingGlobe", "nonMovingGlobe")) %>%
  select(-moveGlobeWhileScaling) %>%
  inner_join(demographic, by = "UserID") %>%
  inner_join(scale_MG, by = "UserID") %>%
    rename(
    PAAS_MG = Mentally_demanding,
    BORG_MG = Physically_demanding
  ) %>%
  mutate(
    PAAS_MG = as.numeric(str_extract(PAAS_MG, "\\d+(\\.\\d+)?")),
    BORG_MG = as.numeric(str_extract(BORG_MG, "\\d+(\\.\\d+)?"))
  ) %>%
  mutate(
    PAAS_MG = if_else(scaleCondition == "movingGlobe", PAAS_MG, NA_real_),
    BORG_MG = if_else(scaleCondition == "movingGlobe", BORG_MG, NA_real_)
  ) %>%
  inner_join(scale_NMG, by = "UserID") %>%
  rename(
    PAAS_NMG = Mentally_demanding,
    BORG_NMG = Physically_demanding
  ) %>%
  mutate(
    PAAS_NMG = as.numeric(str_extract(PAAS_NMG, "\\d+(\\.\\d+)?")),
    BORG_NMG = as.numeric(str_extract(BORG_NMG, "\\d+(\\.\\d+)?"))
  ) %>%
  mutate(
    PAAS_NMG = if_else(scaleCondition == "nonMovingGlobe", PAAS_NMG, NA_real_),
    BORG_NMG = if_else(scaleCondition == "nonMovingGlobe", BORG_NMG, NA_real_)
  ) %>%
  inner_join(scale_preference, by = "UserID") %>%
  rename(
    behaviour_preference = Scale_preference,
    behaviour_feedback = Scale_feedback
  ) %>%
  mutate(
    behaviour_preference = case_when(
    str_detect(behaviour_preference, "Maintain distance") ~ "maintainDistance",
    str_detect(behaviour_preference, "Maintain globe") ~ "maintainGlobe",
    str_detect(behaviour_preference, "no preference") ~ "noPreference",
    TRUE ~ "unknown"
  )) %>%
  filter(Type == "scaleTask") %>%
  select(UserID, TaskID, ActionID, scaleCondition, zoomDirection, Date, ActionStatus, main_scale_x,
  main_scale_y, main_scale_z, target_scale_x, target_scale_y, target_scale_z, match_accuracy_result, status,
  PAAS_MG, BORG_MG, PAAS_NMG, BORG_NMG, behaviour_preference, behaviour_feedback) %>%
  mutate(zoomDirection = as.factor(zoomDirection), 
         scaleCondition = as.factor(scaleCondition),
         status = as.factor(status),
         behaviour_preference = as.factor(behaviour_preference))

Scale Task Study

Accuracy
Normality
data.scale.matched <- data.scale %>%
  filter(status == "Matched")

shapiro.test(data.scale.matched$match_accuracy_result)
## 
##  Shapiro-Wilk normality test
## 
## data:  data.scale.matched$match_accuracy_result
## W = 0.94732, p-value = 1.64e-06
hist(data.scale.matched$match_accuracy_result, breaks = 100,
     main = "Histogram (Zoomed)", xlab = "Accuracy",
     col = "lightblue", xlim = c(0, 0.5))

plot(density(data.scale.matched$match_accuracy_result), 
     main = "Density Plot (Zoomed)", xlab = "Accuracy",
     col = "blue", lwd = 2, xlim = c(0, 0.5))

# Although the w value is close to 1, the p value is below 0.05 so we reject null hypothesis that the data is normally distributed
# So, we cannot use one way ANOVA, instead, we use Wilcoxon signed-rank test
Statistical tests
data.scale.matched.accuracy_avg.long <- data.scale.matched %>%
  group_by(UserID, scaleCondition, zoomDirection) %>%
  summarise(mean_accuracy = mean(match_accuracy_result, na.rm = TRUE), .groups = 'drop')

data.scale.matched.art <- art(mean_accuracy ~ scaleCondition * zoomDirection + (1|UserID), data = data.scale.matched.accuracy_avg.long)

anova(data.scale.matched.art)
## Analysis of Variance of Aligned Rank Transformed Data
## 
## Table Type: Analysis of Deviance Table (Type III Wald F tests with Kenward-Roger df) 
## Model: Mixed Effects (lmer)
## Response: art(mean_accuracy)
## 
##                                      F Df Df.res  Pr(>F)  
## 1 scaleCondition               0.54873  1     33 0.46407  
## 2 zoomDirection                0.51402  1     33 0.47845  
## 3 scaleCondition:zoomDirection 0.95304  1     33 0.33605  
## ---
## Signif. codes:   0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# • The main effect of scaleCondition on mean accuracy was not significant, F(1, 33) = 0.55, p = 0.464.
# • The main effect of zoomDirection was also not significant, F(1, 33) = 0.51, p = 0.478.
# • The interaction between scaleCondition and zoomDirection was not significant either, F(1, 33) = 0.95, p = 0.336.

# There were no statistically significant effects of either scale condition, zoom direction, or their interaction on mean accuracy. This suggests that participants’ accuracy was not influenced by how the globe was scaled or zoomed, nor by the combination of these two factors.

ggplot(data.scale.matched.accuracy_avg.long, aes(x = scaleCondition, y = mean_accuracy, group = UserID)) +
  geom_line(aes(color = as.factor(UserID)), linewidth = 1, alpha = 0.6) +
  geom_point(size = 3) +
  facet_wrap(~ zoomDirection) + 
  labs(title = "Paired Accuracy by Globe Behaviour and Zoom Direction",
       x = "Behaviour",
       y = "Match Accuracy",
      color = "UserID") +
  theme_minimal() +
  theme(strip.text = element_text(size = 12, face = "bold"))

ggplot(data.scale.matched.accuracy_avg.long, aes(x = scaleCondition, y = mean_accuracy)) +
  geom_boxplot(outlier.shape = NA, fill = "lightblue") +
  geom_jitter(width = 0.1, size = 2, alpha = 0.7) +
  facet_wrap(~ zoomDirection) + 
  labs(title = "Boxplots of Accuracy by Globe Movement Behaviour",
       x = "Behaviour",
       y = "Match Accuracy") +
  theme_minimal()

Completion Time
Normality
data.scale.taskCompletion_avg <- data.scale %>%
  group_by(UserID, TaskID) %>%
  summarise(
    completion_time = as.numeric(difftime(max(Date), min(Date), units = "mins")),
    .groups = "drop"
  )

shapiro.test(data.scale.taskCompletion_avg$completion_time)
## 
##  Shapiro-Wilk normality test
## 
## data:  data.scale.taskCompletion_avg$completion_time
## W = 0.69808, p-value < 2.2e-16
Statistical Tests
data.scale.taskCompletion_avg.long <- data.scale %>%
  group_by(UserID, scaleCondition, TaskID, zoomDirection) %>%
  summarise(
    completion_time = as.numeric(difftime(max(Date), min(Date), units = "mins")),
    .groups = "drop"
  ) %>%
  group_by(UserID, scaleCondition, zoomDirection) %>%
  summarise(
    avg_completion_time = mean(completion_time),
    .groups = "drop"
  ) 

data.scale.taskCompletion.art <- art(avg_completion_time ~ scaleCondition * zoomDirection + (1|UserID), data = data.scale.taskCompletion_avg.long)

anova(data.scale.matched.art)
## Analysis of Variance of Aligned Rank Transformed Data
## 
## Table Type: Analysis of Deviance Table (Type III Wald F tests with Kenward-Roger df) 
## Model: Mixed Effects (lmer)
## Response: art(mean_accuracy)
## 
##                                      F Df Df.res  Pr(>F)  
## 1 scaleCondition               0.54873  1     33 0.46407  
## 2 zoomDirection                0.51402  1     33 0.47845  
## 3 scaleCondition:zoomDirection 0.95304  1     33 0.33605  
## ---
## Signif. codes:   0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# scaleCondition F-value 0.549 P-value 0.464 means not significant
# zoomDirection F-value 0.514 P-value 0.478 means Not significant
# scaleCondition:zoomDirection (Interaction) F-value 0.953 Not significant

# The analysis revealed no statistically significant effects of scaleCondition, zoomDirection, or their interaction on mean accuracy. All p-values are well above the common alpha level of 0.05, indicating that neither the type of scaling nor the zoom direction had a meaningful impact on participants’ accuracy. Additionally, the lack of a significant interaction suggests that the combined effect of scaling and zoom direction does not influence accuracy either.


ggplot(data.scale.taskCompletion_avg.long, aes(x = scaleCondition, y = avg_completion_time, group = UserID)) +
  geom_line(aes(color = as.factor(UserID)), linewidth = 1, alpha = 0.6) +
  geom_point(size = 3) +
  facet_wrap(~ zoomDirection) + 
  labs(
    title = "Paired Task Completion Time by Behaviour and Zoom Direction",
    x = "Behaviour",
    y = "Completion Time (minutes)",
    color = "UserID"
  ) +
  theme_minimal()

ggplot(data.scale.taskCompletion_avg.long, aes(x = scaleCondition, y = avg_completion_time)) +
  geom_boxplot(outlier.shape = NA, fill = "lightblue") +
  geom_jitter(width = 0.1, size = 2, alpha = 0.7) +
  facet_wrap(~ zoomDirection) + 
  labs(title = "Boxplots of Task Completion Time by Globe's Behaviour and Zoom Direction",
       x = "Behaviour",
       y = "Task Completion Time") +
  theme_minimal()

Subjective Measures
Physical and Mental Exertion
data.scale.long <- data.scale %>% 
  select(UserID, scaleCondition, PAAS_MG, BORG_MG, PAAS_NMG, BORG_NMG) %>%
  distinct() %>%
  pivot_longer(
    cols = c(PAAS_MG, BORG_MG, PAAS_NMG, BORG_NMG),
    names_to = "Measure",
    values_to = "Score"
  ) %>%
  filter(!is.na(Score)) %>%  
  mutate(
    ExertionType = case_when(
      str_detect(Measure, "PAAS") ~ "Cognitive load",
      str_detect(Measure, "BORG") ~ "Physical exertion",
      TRUE ~ "Unknown"
    ),
    UserID = as.factor(UserID),
    ExertionType = as.factor(ExertionType)
  ) %>%
  group_by(UserID, scaleCondition, Measure)
  
data.scale.long.art_anova <- art(Score ~ scaleCondition * ExertionType + (1|UserID), data = data.scale.long)
anova(data.scale.long.art_anova)
## Analysis of Variance of Aligned Rank Transformed Data
## 
## Table Type: Analysis of Deviance Table (Type III Wald F tests with Kenward-Roger df) 
## Model: Mixed Effects (lmer)
## Response: art(Score)
## 
##                                       F Df Df.res     Pr(>F)    
## 1 scaleCondition               1.358691  1     33    0.25212    
## 2 ExertionType                55.274139  1     33 1.5246e-08 ***
## 3 scaleCondition:ExertionType  0.084177  1     33    0.77353    
## ---
## Signif. codes:   0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#   •   Main effect of scaleCondition: F(1, 33) = 1.36, p = 0.2521 Not statistically significant. This suggests that the scaling condition (e.g. scaled vs not scaled globe) did not have a meaningful effect on exertion scores overall. 
#   •   Main effect of ExertionType: F(1, 33) = 55.27, p < 0.001 → Highly significant. This shows a strong difference between cognitive load (PAAS) and physical exertion (BORG) scores, regardless of the scale condition.
#   •   Interaction (scaleCondition × ExertionType): F(1, 33) = 0.084, p = 0.7735. This means the difference between PAAS and BORG scores does not vary depending on whether scaling was involved.
# Only the type of exertion (cognitive vs physical) had a significant impact on participant scores. The scale condition had no effect, nor did it change the relationship between exertion types. Participants consistently rated cognitive and physical exertion differently, but this pattern was stable across both scaled and non-scaled conditions.

ggplot(data.scale.long, aes(x = scaleCondition, y = Score, fill = ExertionType)) +
  geom_boxplot(width = 0.1, position = position_dodge(0.8)) +
  labs(
    x = "Scale Condition",
    y = "Score",
    title = "Distribution of Scores by Behaviour and Exertion Type"
  ) +
  theme_minimal()

Preference
data.scale %>%
  select(UserID, behaviour_preference) %>%
  distinct() %>%  
  count(behaviour_preference) %>%
  mutate(
    percent = n / sum(n),
    ncount = paste0(n, "\n", percent_format()(percent))
  ) %>%
  ggplot(aes(x = "", y = n, fill = behaviour_preference)) +
  geom_col(width = 1, color = "white") +
  coord_polar(theta = "y") +
  geom_text(aes(label = ncount), position = position_stack(vjust = 0.5), size = 4) +
  labs(
    title = "Distribution of Scale Behaviour Preferences",
    fill = "Preference"
  ) +
  theme_void()

Comments
data.scale.preference.summary <- data.scale %>%
  mutate(
    behaviour_preference = case_when(
      behaviour_preference == "maintainDistance" ~ "Maintain Globe's Distance",
      behaviour_preference == "maintainGlobe" ~ "Maintain Globe's Position",
      behaviour_preference == "noPreference" ~ "No Preference",
      TRUE ~ behaviour_preference
    )
  ) %>%
  group_by(UserID) %>%
  summarise(
    behaviour_preference = first(behaviour_preference),
    behaviour_feedback = first(behaviour_feedback),
    .groups = "drop"
  )

kable(data.scale.preference.summary, caption = "User Feedback Summary - Scale")
User Feedback Summary - Scale
UserID behaviour_preference behaviour_feedback
1 Maintain Globe’s Distance I prefer maintain globe position since it makes me easy to observe the globe closely and clearly, because I think the maintain distance one is not close enough and a bit blurry.
2 Maintain Globe’s Distance For me personally I like to use the maintain distance to globe behaviour because its easier to see when observing the surface. But depends on the situation, if we are in a bigger room such as auditorium it will be more managable. But since in this I’m doing it in a small room, its easier to use the maintain distance to globe.
3 Maintain Globe’s Distance For the scope of this globe experiment, I prefer “maintain distance…”, because I do not think it is necessary to go inside the globe which is empty. However, the zoom level for “maintain distance…” behaviour needs to be closer or have zoom level control, I.e, “Observing a very small island in the globe, like Bermuda island”
4 Maintain Globe’s Distance I like the 2nd options better so we can observe the globe more detail, without being worry about the globe disappear in front of us.
5 Maintain Globe’s Position I prefer maintain globe position because the zoom level is larger, so I can easily observe the object
6 Maintain Globe’s Distance For specifically observing maps/globes, I prefer the maiaintain distance to globes one because, ithe maximum zoom level is enough for me to observe the surface of the globe.
7 Maintain Globe’s Distance I prefer the maintain distance to globe because it is easier to observe t, the zoom level of maintain distance to globe is good.
8 No Preference Depends on the situation. I have no preference. Maintain distance is confusing whether the gesture is broken or not at the maximum zoom point. But if the purpose is for observing the globe the maintain distance is better.
9 Maintain Globe’s Distance I prefer maintain distance
10 Maintain Globe’s Distance I prefer the maintain distance because it is easier to observe the surface in a proper distance
11 No Preference It depends, for professionals like maybe government, if they want to observe details, it would better use maintain globe. But for casual users, they would not like the globe zoomed through their heads, they would like maintain distance better. So, I have no preference.
12 Maintain Globe’s Distance 1. It would be better if the limit of the zoom is very close (increase the limit) to our face or at least give the option to.
  1. The DPI and the speed of scaling in and out need to be adjusted. It is more into the callibration of the hands gesture and the actual object.
  2. It is difficult to combine the zoom gestures and the rotation with two hands. |
Summary